﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Win32.SafeHandles;
using NHook.Native;

namespace NHook
{
	public class Disasm
	{
		public VA Address
		{
			get;
			set;
		}
		public DisasmResult Instruction
		{
			get;
			set;
		}
	}
	public interface Commandable
	{
		Wait Wait
		{
			get;
		}
		Continue Continue
		{
			get;
		}
		ProcessDebugger ProcessDebugger
		{
			get;
		}
		ThreadDebugger GetCurrentThreadDebugger();
		DebugEvent CurrentEvent
		{
			get;
		}
	}
	public class Continue
	{
		Commandable _Commandable;
		public Continue(Commandable commandable)
		{
			_Commandable = commandable;
		}

		public DebugEventEx<ExceptionDetail> UntilNextBreakpoint()
		{
			return (DebugEventEx<ExceptionDetail>)Until(ev =>
			{
				if(ev.EventType != DebugEventType.ExceptionEvent)
					return false;
				return ((DebugEventEx<ExceptionDetail>)ev).Details.Exception.Reason == ExceptionReason.ExceptionBreakpoint;
			});
		}
		public DebugEventEx<ExceptionDetail> UntilBreakpoint(Breakpoint breakPoint)
		{
			return (DebugEventEx<ExceptionDetail>)Until(ev =>
			{
				var exceptionEvt = ev.As<ExceptionDetail>();
				if(exceptionEvt == null)
					return false;
				if(exceptionEvt.Exception.Reason != ExceptionReason.ExceptionBreakpoint)
					return false;
				return exceptionEvt.Exception.Address == breakPoint.Address.ToIntPtr();
			});
		}

		public int ToEnd()
		{
			var evt = Until<ExitProcessDetail>();
			Run();
			return evt.Details.ExitCode;
		}

		public DebugEvent Until(DebugEventType debugEventType)
		{
			return Until(evt => evt.EventType == debugEventType);
		}

		public DebugEventEx<T> Until<T>() where T : Detail
		{
			return (DebugEventEx<T>)Until(evt => evt is DebugEventEx<T>);
		}

		public DebugEvent Until(Func<DebugEvent, bool> filter)
		{
			DebugEvent lastEvent = null;
			bool first = true;
			if(_Commandable.CurrentEvent != null)
				Run();
			while(lastEvent == null || !filter(lastEvent))
			{
				if(!first)
					Run();
				first = false;
				lastEvent = _Commandable.Wait.NextEvent();
			}
			return lastEvent;
		}

		public void Run()
		{
			Run(true);
		}

		internal event Action OnNextContinuing;



		public void Run(bool handleException)
		{
			if(_Commandable.CurrentEvent == null)
				throw new InvalidOperationException("No debugging event happened yet, there is nothing to continue");
			_Commandable.ProcessDebugger.ContinueNextEvent(_Commandable.CurrentEvent, handleException);
		}

		internal void FireContinuing()
		{
			var thread = _Commandable.GetCurrentThreadDebugger();
			if(thread != null && thread.StepByStep)
				NHook.Native.Helper.StepByStep(thread.Handle, true);
			if(OnNextContinuing != null)
			{
				OnNextContinuing();
				OnNextContinuing = null;
			}
		}

		public void UntilDllLoaded(string dllName)
		{
			Until(ev =>
			{
				var loading = ev.As<LoadDllDetail>();
				if(loading == null)
					return false;
				return ProcessDebugger.AreSameImage(loading.ImageName, dllName);
			});
		}


		public DebugEvent UntilNoBreakpointOrSingleStep()
		{
			return Until(ev =>
				{
					var exception = ev.As<ExceptionDetail>();
					if(exception == null)
						return true;
					return exception.Exception.Reason != ExceptionReason.ExceptionBreakpoint && exception.Exception.Reason != ExceptionReason.ExceptionSingleStep;
				});
		}
		const int MAXCMDSIZE = 16;



		public Disasm UntilInstruction(string instruction)
		{
			return UntilInstruction(disasm => disasm.Instruction.Instruction.StartsWith(instruction));
		}
		public Disasm UntilInstruction(Func<Disasm,bool> predicate)
		{
			var thread = _Commandable.GetCurrentThreadDebugger();
			Disasm result = new Disasm();
			bool hasSetStepByStep = false;
			if(!thread.StepByStep)
			{
				hasSetStepByStep = true;
				thread.StepByStep = true;
			}
			try
			{
				Until(ev =>
				{
					var exception = ev.As<ExceptionDetail>();
					if(exception == null || exception.Exception.Reason != ExceptionReason.ExceptionSingleStep)
						return false;
					var memory = MemoryUtil.ReadMemory(
						_Commandable.ProcessDebugger.Debuggee.Information.ProcessHandle.DangerousGetHandle(),
						exception.Exception.Address, 
						MAXCMDSIZE);

					var disasm = DisasmWrapper.Disasm(memory);
					result.Instruction = disasm;
					result.Address = new VA(ev.As<ExceptionDetail>().Exception.Address);

					if(predicate(result))
					{
						return true;
					}
					else
						return false;
				});
			}
			finally
			{
				if(hasSetStepByStep)
					thread.StepByStep = false;
			}
			return result;
		}
	}
	public class Wait
	{
		Commandable _Commandable;
	
		

		public Wait(Commandable commandable)
		{
			_Commandable = commandable;
		}
		event Func<DebugEvent, NHook.ProcessDebugger.WaitBehavior> OnNextWait;

		public DebugEvent NextEvent()
		{
			NHook.ProcessDebugger.WaitBehavior waitBehavior = NHook.ProcessDebugger.WaitBehavior.Skip;
			DebugEvent dbgEvent = null;
			bool isProcessDebugger = _Commandable.ProcessDebugger == _Commandable;
			while(waitBehavior == NHook.ProcessDebugger.WaitBehavior.Skip)
			{
				while(true)
				{
					dbgEvent = _Commandable.ProcessDebugger.WaitNextEvent(out waitBehavior);
					if(isProcessDebugger)
						break;
					if(_Commandable.GetCurrentThreadDebugger().ThreadId == dbgEvent.ThreadId)
						break;
				}
				var currentThread = _Commandable.GetCurrentThreadDebugger();

				if(dbgEvent.As<ExceptionDetail>() != null)
				{
					var exception = dbgEvent.As<ExceptionDetail>().Exception;
					Breakpoint breakpoint = null;
					if(exception.Reason == ExceptionReason.ExceptionBreakpoint)
						if(_Commandable.ProcessDebugger.Breakpoints.TryGetValue(new VA(exception.Address), out breakpoint))
						{
							currentThread.GoTo(breakpoint.Address);
							bool haveSetStep = false;
							_Commandable.Continue.OnNextContinuing += () => //Disable the breakpoint, run from the original instruction, activate step in
							{
								breakpoint.Disable();
								currentThread.GoTo(new VA(exception.Address));
								if(!currentThread.StepByStep)
								{
									currentThread.StepByStep = true;
									haveSetStep = true;
								}
							};
							OnNextWait += (DebugEvent waitEvent) => //Enable breakpoint for next time, turn step by step off if last breakpoint set it
							{
								if(waitEvent.As<ExceptionDetail>() != null)
								{
									var nextException = waitEvent.As<ExceptionDetail>().Exception;
									if(nextException.Reason == ExceptionReason.ExceptionSingleStep)
									{
										breakpoint.Enable();
										if(haveSetStep)
										{
											currentThread.StepByStep = false;
											return NHook.ProcessDebugger.WaitBehavior.Skip;
										}
									}
								}
								return NHook.ProcessDebugger.WaitBehavior.Keep;
							};
						}
				}
				if(waitBehavior == NHook.ProcessDebugger.WaitBehavior.Skip)
					_Commandable.Continue.Run(true);
			}
			return dbgEvent;
		}

		internal NHook.ProcessDebugger.WaitBehavior FireOnWaitNext(DebugEvent dbgEvent)
		{
			NHook.ProcessDebugger.WaitBehavior waitBehavior = NHook.ProcessDebugger.WaitBehavior.Keep;
			if(OnNextWait != null)
			{
				waitBehavior = OnNextWait(dbgEvent);
				OnNextWait = null;
			}
			return waitBehavior;
		}
	}
}
